package org.moparscape.msc.gs.core;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.TreeMap;

import org.apache.mina.common.IoSession;
import org.moparscape.msc.config.Config;
import org.moparscape.msc.gs.Instance;
import org.moparscape.msc.gs.connection.RSCPacket;
import org.moparscape.msc.gs.connection.filter.IPBanManager;
import org.moparscape.msc.gs.model.Player;
import org.moparscape.msc.gs.model.World;
import org.moparscape.msc.gs.model.landscape.ActiveTile;
import org.moparscape.msc.gs.phandler.PacketHandler;
import org.moparscape.msc.gs.phandler.PacketHandlerDef;
import org.moparscape.msc.gs.util.Logger;

/**
 * The central motor of the game. This class is responsible for the primary
 * operation of the entire game.
 */
public final class GameEngine extends Thread {

    /**
     * World instance
     */
    private static final World world = Instance.getWorld();

    /**
     * Responsible for updating all connected clients
     */
    private ClientUpdater clientUpdater = new ClientUpdater();
    /**
     * Handles delayed events rather than events to be ran every iteration
     */
    private DelayedEventHandler eventHandler = new DelayedEventHandler();
    private long lastCleanedChatlogs = 0;
    private int lastCleanedChatlogsOutput = 0;
    /**
     * The mapping of packet IDs to their handler
     */
    private TreeMap<Integer, PacketHandler> packetHandlers = new TreeMap<>();

    /**
     * Only use this method when you need the actual time.
     */
    public static long getTimestamp() {
        return System.currentTimeMillis();
    }

    /**
     * Constructs a new game engine with an empty packet queue.
     */
    public GameEngine() {
        try {
            loadPacketHandlers();
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(0);
        }
        redirectSystemStreams();
    }

    public void emptyWorld() {
        for (Player p : world.getPlayers()) {
            p.save();
            p.getActionSender().sendLogout();
        }
    }

    /**
     * Loads the packet handling classes from the persistence manager.
     *
     * @throws Exception
     */
    protected void loadPacketHandlers() throws Exception {
        PacketHandlerDef[] handlerDefs = Instance.dataStore()
                .loadPacketHandlerDefs();
        for (PacketHandlerDef handlerDef : handlerDefs) {
            try {
                String className = handlerDef.getClassName();
                Class<?> c = Class.forName(className);
                if (c != null) {
                    PacketHandler handler = (PacketHandler) c.newInstance();
                    for (int packetID : handlerDef.getAssociatedPackets()) {
                        packetHandlers.put(packetID, handler);
                    }
                }
            } catch (Exception e) {
                Logger.error(e);
            }
        }
    }

    private void processClients() {
        clientUpdater.sendQueuedPackets();
        clientUpdater.doMajor();//usually runs at 600
        clientUpdater.doMinor();//usually runs at 114?
    }

    private void processEvents() {
        eventHandler.doEvents();
    }

    public DelayedEventHandler getEventHandler() {
        return eventHandler;
    }

    /**
     * Redirects system err
     */
    public static void redirectSystemStreams() {
        OutputStream out = new OutputStream() {
            @Override
            public void write(int b) throws IOException {
                String line = String.valueOf((char) b);
                Logger.systemerr(line);
            }

            @Override
            public void write(byte[] b, int off, int len) throws IOException {
                String line = new String(b, off, len);
                Logger.systemerr(line);
            }

            @Override
            public void write(byte[] b) throws IOException {
                write(b, 0, b.length);
            }
        };
        System.setErr(new PrintStream(out, true));
    }

    public void submitIncomingPacket(RSCPacket p) {
            IoSession session = p.getSession();
            Player player = (Player) session.getAttachment();
            if (player.getUsername() == null && p.getID() != 32 && p.getID() != 77 && p.getID() != 0) {
                final String ip = player.getCurrentIP();
                IPBanManager.throttle(ip);
                return;
            }
            PacketHandler handler = packetHandlers.get(p.getID());
            if (handler != null) {
                try {
                    handler.handlePacket(p, session);
                } catch (Exception e) {
                    Logger.error("Exception with p[" + p.getID() + "] from "+ player.getUsername() + " [" + player.getCurrentIP() + "]: ");
                    player.getActionSender().sendLogout();
                    player.destroy(false);
                }
            } else {
                Logger.error("Unhandled packet from " + player.getCurrentIP()  + ": " + p.getID() + "len: " + p.getLength());
            }
    }

    /**
     * The thread execution process.
     */
    public void run() {
        Logger.println("GameEngine now running");
        World.getTaskManager().scheduledProcessor(new Runnable() {
            @Override
            public void run() {
                garbageCollect();
                World.getTaskManager().scheduledProcessor(this, Config.GARBAGE_COLLECT_INTERVAL);
            }
        }, Config.GARBAGE_COLLECT_INTERVAL);
        World.getTaskManager().scheduledProcessor(new Runnable() {
            @Override
            public void run() {
                for (Player p : world.getPlayers()) {
                    p.save();
                }
                World.getTaskManager().scheduledProcessor(this, Config.SAVE_INTERVAL);
            }
        }, Config.SAVE_INTERVAL);
        World.getTaskManager().scheduledProcessor(new Runnable() {
            @Override
            public void run() {
                processClients();
                processEvents(); //runs at 100 but i think i saw a 500 delay? we will have it run on the 600 mili till
                World.getTaskManager().scheduledProcessor(this, 600);
            }
        }, 600);
    }

    /**
     * Cleans garbage
     */
    public synchronized void garbageCollect() {
        for (int i = 0; i < Instance.getWorld().tiles.length; i++) {
            for (int in = 0; in < Instance.getWorld().tiles[i].length; in++) {
                ActiveTile tile = Instance.getWorld().tiles[i][in];
                if (tile != null) {
                    if (!tile.hasGameObject() && !tile.hasItems()
                            && !tile.hasNpcs() && !tile.hasPlayers()) {
                        Instance.getWorld().tiles[i][in] = null;
                    }
                }
            }
        }
        Runtime.getRuntime().gc();
        int curMemory = (int) (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1000;
        int newMemory = (int) (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1000;
        Logger.println("GARBAGE COLLECT | Executing Memory Cleanup");
        Logger.println("GARBAGE COLLECT | Memory before: " + curMemory
                + " Memory after: " + newMemory + " (Freed: "
                + (curMemory - newMemory) + "kb)");
    }
}
